﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.Script.Serialization;
using ExtendedMvc.Common.Diagnostics;

namespace ExtendedMvc {
	/// <summary>
	/// Represents support for rendering HTML controls in a strongly typed view
	/// </summary>
	/// <typeparam name="TModel">The type of the model.</typeparam>
	public class ModelViewHtmlHelper<TModel> : ModelViewHelper<TModel> where TModel : class {
		/// <summary>
		/// Initializes a new instance of the <see cref="ModelViewHtmlHelper&lt;TModel&gt;"/> class.
		/// </summary>
		/// <param name="viewDataContainer">The view data container.</param>
		/// <param name="viewContext">The view context.</param>
		public ModelViewHtmlHelper(IViewDataContainer<TModel> viewDataContainer, ViewContext viewContext) : base(viewDataContainer, viewContext, false) { }
	}

	/// <summary>
	/// Represents support for rendering HTML controls in a view
	/// </summary>
	/// <typeparam name="TModel">The type of the model.</typeparam>
	public class ModelViewHtmlHelper : ModelViewHelper {
		/// <summary>
		/// Initializes a new instance of the <see cref="ModelViewHtmlHelper&lt;TModel&gt;"/> class.
		/// </summary>
		/// <param name="viewDataContainer">The view data container.</param>
		/// <param name="viewContext">The view context.</param>
		public ModelViewHtmlHelper(IViewDataContainer viewDataContainer, ViewContext viewContext) : base(viewDataContainer, viewContext, false) { }
	}

	/// <summary>
	/// Represents support for rendering Ajax controls in a strongly typed view
	/// </summary>
	/// <typeparam name="TModel">The type of the model.</typeparam>
	public class ModelViewAjaxHelper<TModel> : ModelViewHelper<TModel> where TModel : class {
		/// <summary>
		/// Initializes a new instance of the <see cref="ModelViewAjaxHelper&lt;TModel&gt;"/> class.
		/// </summary>
		/// <param name="viewDataContainer">The view data container.</param>
		/// <param name="viewContext">The view context.</param>
		public ModelViewAjaxHelper(IViewDataContainer<TModel> viewDataContainer, ViewContext viewContext) : base(viewDataContainer, viewContext, true) { }
	}

	/// <summary>
	/// Represents support for rendering Ajax controls in a view
	/// </summary>
	/// <typeparam name="TModel">The type of the model.</typeparam>
	public class ModelViewAjaxHelper : ModelViewHelper {
		/// <summary>
		/// Initializes a new instance of the <see cref="ModelViewAjaxHelper&lt;TModel&gt;"/> class.
		/// </summary>
		/// <param name="viewDataContainer">The view data container.</param>
		/// <param name="viewContext">The view context.</param>
		public ModelViewAjaxHelper(IViewDataContainer viewDataContainer, ViewContext viewContext) : base(viewDataContainer, viewContext, true) { }
	}

	/// <summary>
	/// Represents support for rendering controls in a strongly typed view
	/// </summary>
	/// <typeparam name="TModel">The type of the model.</typeparam>
	public abstract class ModelViewHelper<TModel> : ModelViewHelper, IModelViewHelper<TModel> where TModel : class {
		// Fields
		public new IViewDataContainer<TModel> ViewDataContainer { get; private set; }
		public new ViewDataDictionary<TModel> ViewData { get; private set; }

		public ModelViewHelper(IViewDataContainer<TModel> viewDataContainer, ViewContext viewContext, bool useAjax)
			: base(viewDataContainer, viewContext, useAjax) {
			this.ViewData = new ViewDataDictionary<TModel>(viewDataContainer.ViewData);
			this.ViewDataContainer = viewDataContainer;
		}
	}

	/// <summary>
	/// Represents support for rendering controls in a view
	/// </summary>
	/// <typeparam name="TModel">The type of the model.</typeparam>
	public abstract class ModelViewHelper : IModelViewHelper {
		private static string _globalizationScriptPath;

		// Fields
		public bool UseAjax { get; private set; }
		public ViewContext ViewContext { get; private set; }
		public IViewDataContainer ViewDataContainer { get; private set; }

		public ViewDataDictionary ViewData { get { return this.ViewDataContainer.ViewData; } }
		public RouteCollection RouteCollection { get { return RouteTable.Routes; } }

		static ModelViewHelper() { }

		public ModelViewHelper(IViewDataContainer viewDataContainer, ViewContext viewContext, bool useAjax) {
			Check.Argument.IsNotNull(viewContext, "viewContext");
			Check.Argument.IsNotNull(viewDataContainer, "viewDataContainer");

			this.UseAjax = useAjax;
			this.ViewContext = viewContext;
			this.ViewDataContainer = viewDataContainer;
		}

		public ModelMetadata GetMetadataForType(Type modelType) {
			return ModelMetadataProviders.Current.GetMetadataForType(null, modelType);
		}

		public ModelMetadata GetMetadataForProperty(string propertyName) {
			if (ViewContext.ViewData.ModelMetadata.Model is IEnumerable<object>) return GetMetadataForType(ViewContext.ViewData.ModelMetadata.ModelType.GetGenericArguments()[0]).Properties.SingleOrDefault(p => p.PropertyName == propertyName);
			else return ViewContext.ViewData.ModelMetadata.Properties.SingleOrDefault(p => p.PropertyName == propertyName);
		}

		internal static IView FindPartialView(ViewContext viewContext, string viewName, ViewEngineCollection viewEngineCollection) {
			var result = viewEngineCollection.FindPartialView(viewContext, viewName);
			if (result.View != null) return result.View;
			var Builder = new StringBuilder();
			foreach (string content in result.SearchedLocations) {
				Builder.AppendLine();
				Builder.Append(content);
			}
			throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, "The partial view '{0}' was not found. The following locations were searched:{1}", new object[] { viewName, Builder }));
		}

		internal virtual ViewDataDictionary MergeViewData(object model, ViewDataDictionary viewData) {
			ViewDataDictionary dictionary = null;
			if (model == null) {
				dictionary = new ViewDataDictionary(viewData ?? this.ViewContext.ViewData);
			} else if (viewData == null) {
				dictionary = new ViewDataDictionary(model);
			} else {
				dictionary = new ViewDataDictionary(viewData);
				dictionary.Model = model;
			}
			return dictionary;
		}

		internal virtual void RenderPartialContent(string viewName, object model, ViewDataDictionary viewData, TextWriter writer, ViewEngineCollection viewEngineCollection) {
			Check.Argument.IsNotNullOrEmpty(viewName, "viewName");
			var viewContext = new ViewContext(this.ViewContext, this.ViewContext.View, MergeViewData(model, viewData), this.ViewContext.TempData, writer);
			FindPartialView(viewContext, viewName, viewEngineCollection).Render(viewContext, writer);
		}

		private RouteValueDictionary MergeDictionaries(params RouteValueDictionary[] dictionaries) {
			var result = new RouteValueDictionary();
			foreach (var dictionary in Enumerable.Where<RouteValueDictionary>(dictionaries, d => d != null)) {
				foreach (KeyValuePair<string, object> pair in dictionary) {
					if (!result.ContainsKey(pair.Key)) result.Add(pair.Key, pair.Value);
				}
			}
			return result;
		}

		private RouteData CreateRouteData(RouteBase route, RouteValueDictionary routeValues, RouteValueDictionary dataTokens, ViewContext parentViewContext) {
			var data = new RouteData();
			foreach (KeyValuePair<string, object> value in routeValues)
				data.Values.Add(value.Key, value.Value);
			foreach (KeyValuePair<string, object> token in dataTokens)
				data.DataTokens.Add(token.Key, token.Value);
			data.Route = route;
			data.DataTokens["ParentActionViewContext"] = parentViewContext;
			return data;
		}

		public string GetAreaName(RouteBase routeBase) {
			var area = routeBase as IRouteWithArea;
			if (area != null) return area.Area;
			var route = routeBase as Route;
			if ((route != null) && (route.DataTokens != null)) return (route.DataTokens["area"] as string);
			return null;
		}

		private bool IsUsingArea(RouteCollection routes) {
			return routes.Any(r => !string.IsNullOrEmpty(GetAreaName(r)));
		}

		internal virtual void RenderPartialContent(string actionName, string controllerName, RouteValueDictionary routeValues, TextWriter textWriter) {
			routeValues = MergeDictionaries(new RouteValueDictionary[] { routeValues, this.ViewContext.RouteData.Values });
			if (!string.IsNullOrEmpty(actionName)) routeValues["action"] = actionName;
			if (!string.IsNullOrEmpty(controllerName)) routeValues["controller"] = controllerName;
			var routePath = this.RouteCollection.GetVirtualPathForArea(this.ViewContext.RequestContext, routeValues);
			if (routePath == null) throw new InvalidOperationException("No route in the route table matches the supplied values.");
			if (IsUsingArea(this.RouteCollection)) routeValues.Remove("area");
			RenderRouteContent(routePath, routeValues, textWriter);
		}

		internal virtual void RenderPartialContent(string routeName, string protocol, string hostName, RouteValueDictionary routeValues, TextWriter textWriter) {
			Check.Argument.IsNotNullOrEmpty(routeName, "routeName");
			routeValues = MergeDictionaries(new RouteValueDictionary[] { routeValues, this.ViewContext.RouteData.Values });
			if (!string.IsNullOrEmpty(protocol)) routeValues["protocol"] = protocol;
			if (!string.IsNullOrEmpty(hostName)) routeValues["hostName"] = hostName;
			var routePath = this.RouteCollection.GetVirtualPath(this.ViewContext.RequestContext, routeName, routeValues);
			if (routePath == null) throw new InvalidOperationException("No route in the route table matches the supplied values.");
			if (IsUsingArea(this.RouteCollection)) routeValues.Remove("area");
			RenderRouteContent(routePath, routeValues, textWriter);
		}

		internal virtual void RenderRouteContent(VirtualPathData routePath, RouteValueDictionary routeValues, TextWriter textWriter) {
			var routeData = CreateRouteData(routePath.Route, routeValues, routePath.DataTokens, this.ViewContext);
			var httpContext = this.ViewContext.HttpContext;
			var context = new RequestContext(httpContext, routeData);
			var httpHandler = new ChildActionMvcHandler(context);
			httpContext.Server.Execute(httpHandler, textWriter, true);
		}

		internal class ChildActionMvcHandler : MvcHandler {
			// Methods
			public ChildActionMvcHandler(RequestContext context) : base(context) { }

			protected override void AddVersionHeader(HttpContextBase httpContext) { }
		}

		public static string GlobalizationScriptPath {
			get { return string.IsNullOrEmpty(_globalizationScriptPath) ? "~/Scripts/Globalization" : _globalizationScriptPath; }
			set { _globalizationScriptPath = value; }
		}

		public string JavaScriptStringEncode(string message) {
			if (string.IsNullOrEmpty(message)) return message;
			var output = new StringBuilder();
			new JavaScriptSerializer().Serialize(message, output);
			return output.ToString(1, output.Length - 2);
		}

		#region IModelViewHelper Members

		void IModelViewHelper.RenderPartialContent(string viewName, object viewModel, ViewDataDictionary viewData, TextWriter writer, ViewEngineCollection viewEngineCollection) {
			RenderPartialContent(viewName, viewModel, viewData, writer, viewEngineCollection);

		}

		void IModelViewHelper.RenderPartialContent(string actionName, string controllerName, RouteValueDictionary routeValues, TextWriter textWriter) {
			RenderPartialContent(actionName, controllerName, routeValues, textWriter);
		}

		void IModelViewHelper.RenderPartialContent(string routeName, string protocol, string hostName, RouteValueDictionary routeValues, TextWriter textWriter) {
			RenderPartialContent(routeName, protocol, hostName, routeValues, textWriter);
		}

		#endregion
	}
}
